一种通过后端编译优化脱混淆壳的方法
本文为看雪论坛精华文章
看雪论坛作者ID:baikaishiu
起源
写这个App的时候主要参考了3本书
1. arm_arch_ref
2. Thumb-2SupplementReferenceManual
3. Modern Compiler implementation in c
elf文件格式分析
词法分析
struct arm_inst_ctx {
reg_t ld; // 目的寄存器
reg_t ld2; // 目的寄存器2
reg_t lm; // 参数寄存器1
reg_t ln; // 参数寄存器2
reg_t lp; // 参数寄存器3
int register_list; // 寄存器列表,一般给push和pop用
int imm; // 立即数
int m;
int setflags; // 是否需要跟新flag
int cond; // 条件
int vd; // simd中需要
struct {
unsigned p : 1;
unsigned u : 1; // 在一些立即数中,指出是加还是减
unsigned w : 1;
unsigned t : 2;
};
};
struct arm_inst_desc desclist[] = {
{"0000 o1 i5 lm3 ld3", {t1_inst_lsl, t1_inst_lsr}, {"lsl", "lsr"}},
{"0001 0 i5 lm3 ld3", {t1_inst_asr}, {"asr"}},
{"0001 10 o1 lm3 ln3 ld3", {t1_inst_add, thumb_inst_sub_reg}, {"add", "sub"}},
{"0001 11 o1 i3 ln3 ld3", {t1_inst_add, thumb_inst_sub}, {"add", "sub"}},
{"0010 0 ld3 i8", {thumb_inst_mov}, {"mov"}},
{"0010 1 lm3 i8", {t1_inst_cmp_imm}, {"cmp"}},
{"0011 0 ld3 i8", {t1_inst_add}, {"add"}},
{"0011 1 ld3 i8", {thumb_inst_sub}, {"sub"}},
{"0100 0000 o2 lm3 ld3", {t1_inst_and, thumb_inst_eor, t1_inst_lsl, t1_inst_lsr}, {"and", "eor", "lsl2", "lsr2"}},
{"0100 0001 o2 lm3 ld3", {t1_inst_asr, t1_inst_adc, t1_inst_sbc, t1_inst_ror}, {"asr", "adc", "sbc", "ror"}},
{"0100 0010 0o1 lm3 ld3", {t1_inst_tst, t1_inst_neg}, {"tst", "neg"}},
{"0100 0010 1o1 lm3 ln3", {thumb_inst_cmp, t1_inst_cmn}, {"cmp", "cmn"}},
{"0100 0011 o2 lm3 ld3", {thumb_inst_orr, t1_inst_mul, t1_inst_bic, t1_inst_mvn}, {"orr", "mul", "bic", "mvn"}},
{"0100 0110 d1 lm4 ld3", {t1_inst_mov_0100}, {"mov"}},
{"0100 0100 d1 lm4 ld3", {t1_inst_add}, {"add"}},
{"0100 0101 01 hm3 ln3 ", {thumb_inst_cmp}, {"cmp"}},
{"0100 0101 10 lm3 hn3 ", {thumb_inst_cmp}, {"cmp"}},
{"0100 0101 11 hm3 hn3 ", {thumb_inst_cmp}, {"cmp"}},
{"0100 1 ld3 i8", {thumb_inst_ldr}, {"ldr"}},
{"0100 0111 o1 lm4 000", {t1_inst_bx_0100, thumb_inst_b}, {"bx", "blx"}},
{"0110 0i5 ln3 lm3", {thumb_inst_str}, {"str"}},
{"0110 1i5 lm3 ld3", {thumb_inst_ldr}, {"ldr"}},
{"0111 0 i5 ln3 lm3", {thumb_inst_strb}, {"strb<c> <lp>,[<ln>,#<imm>]"}},
{"0111 1 i5 ln3 ld3", {thumb_inst_ldrb}, {"ldrb<c> <ld>,[<ln>,#<imm5>]"}},
{"1001 0 lm3 i8", {thumb_inst_str}, {"str"}},
{"1001 1 ld3 i8", {thumb_inst_ldr}, {"ldr"}},
{"1010 o1 ld3 i8", {t1_inst_add1, t1_inst_add}, {"add", "add"}},
{"1011 o1 10 m1 rl8", {thumb_inst_push, thumb_inst_pop}, {"push", "pop"}},
{"1011 0000 0 i7", {thumb_inst_add}, {"add"}},
{"1011 0000 1 i7", {thumb_inst_sub}, {"sub"}},
{"1011 1111 c4 i4", {thumb_inst_it}, {"it"}},
{"1100 1 ln3 rl8", {thumb_inst_ldmia}, {"ldmia<c> <Rn>!?, <registers>"}},
{"1101 c4 i8", {thumb_inst_b}, {"b<cond>"}},
{"1110 0 i11", {t1_inst_b}, {"b"}},
{"1110 100p1 u1 1 w1 0 lm4 ln4 lp4 i8", {thumb_inst_strd}, "strd<c>.w"},
{"1110 100p1 u1 1 w1 1 ln4 ld4 le4 i8", {thumb_inst_ldr}, "ldrd<c>.w"},
{"1110 1001 0010 1101 0 m1 0 rl13", {thumb_inst_push}, "push.w"},
{"1110 1000 1011 1101 i1 m1 0 rl13", {thumb_inst_pop}, "pop.w"},
{"1110 1010 010s1 ln4 0 i3 ld4 i2 t2 lm4", {thumb_inst_orr}, "orr{s}<c>.W <ld>,<lm>{, <shift>}"},
{"1110 1101 0d1 10 1101 vd4 1011 i8", {thumb_inst_vpush}, "vpush<c> <list>"},
{"1110 1100 1d1 11 1101 vd4 1011 i8", {thumb_inst_vpop}, "vpop <list>"},
{"1110 1010 1001 ln4 0i3 1111 i2 t2 lm4", {thumb_inst_teq}, "teq<c> <ln>, <lm>{, <shift>}"},
{"111i1 1111 1d1 00 0i3 vd4 i4 0i2 1 i4", {thumb_inst_vmov}, "vmov"},
{"1110 1011 000s1 ln4 0i3 ld4 i2 t2 lm4 ", {thumb_inst_add}, "add{s}<c>.W <ld>,<ln>,<lm>{, <shift>}"},
//{"1111 0i1 01 000s1 1101 0i3 ld4 i8", {thumb_inst_add}, "add{S}<c>.W <Rd>,SP#<const>"},
{"1111 0i1 01 000s1 ln4 0i3 ld4 i8", {thumb_inst_add}, "add{S}<c>.W <Rd>,<Rn>,#<const>"},
{"1111 0s1 c4 i6 10 u1 0 w1 i11", {thumb_inst_b}, "b<c>.w <label>"},
{"1111 0s1 c4 i6 10 u1 1 w1 i11", {thumb_inst_b}, "b<c>.w <label>"},
{"1111 0s1 i10 11 u1 1 w1 i11", {thumb_inst_b}, "bl<c> <label>"},
{"1111 0s1 i10 11 u1 0 w1 i10 0", {thumb_inst_b}, "blx<c> <label>"},
{"1111 0i1 01 101s1 ln4 0i3 ld4 i8", {thumb_inst_sub}, "sub{s}<c>.w <ld>,<ln>,#<const>"},
{"1111 0i1 00010 s1 11110 i3 ld4 i8", {t1_inst_mov_w}, "mov.w"},
{"1111 0i1 101100 i4 0 i3 ld4 i8", {thumb_inst_mov}, "movt"},
{"1111 0i1 100100 i4 0 i3 ld4 i8", {thumb_inst_mov}, "movw"},
{"1111 1000 0001 ln4 ld4 1 p1 u1 w1 i8", {thumb_inst_ldrb}, {"ldrb<c>.w <ld>, [<ln>,#<imm8>]"}},
{"1111 1000 1001 ln4 ld4 i12", {thumb_inst_ldrb}, {"ldrb<c>.w <ld>, [<ln>,#<imm12>]"}},
{"1111 1000 0101 ln4 ld4 0000 00 i2 lm4", {thumb_inst_ldr}, {"ldr<c>.w <ld>, [<ln>,<lm>{,LSL #<shift>}]"}},
{"1111 1000 1101 ln4 ld4 i12", {thumb_inst_ldr}, {"ldr.w"}},
//{"1111 1000 u1 101 1111 ld4 i12", {thumb_inst_ldr}, {"ldr.w"}},
{"1111 1000 1000 ln4 lm4 i12", {thumb_inst_strb}, {"strb<c>.w <Rt>,[<Rn>,#<imm12>]"}},
{"1111 1000 0000 ln4 lm4 1p1 u1 w1 i8", {thumb_inst_strb}, {"strb<c> <Rt>,[<Rn>,#+/-<imm8>]"}},
{"1111 1000 1100 ln4 lm4 i12", {thumb_inst_str}, {"str<c>.w <Rt>,[<Rn>,#<imm12>]"}},
{"1111 1000 0100 ln4 lp4 000000 i2 lm4", {thumb_inst_str}, {"str<c>.w <lo>,[<ln>,<lm>{, LSL #<shift>}]"}},
{"1111 1001 0d1 00 ln4 vd4 i8 lm4", {thumb_inst_vst1}, {"str<c>.w <lo>,[<ln>,<lm>{, LSL #<shift>}]"}},
};
然后基于此,按 nfs -> dfs -> 2d-trans的方式来生成整个状态机图。
nfa的如下:
nfa在归纳同字符集可到达的边,生成dfa:
详细分析可以参考任意一本形式语言与自动机理论,或者 龙书, 《parsing techniques》都可。
语法分析
IT指令处理
语法分析反而比较简单,因为汇编语言不存在特殊的语法,唯一特殊的指令是IT指令,在IDA中arm的IT指令是不会做切分,和上下的代码完整的放到一个block中,比如:
但是假如我们需要用编译优化去掉混淆,必须得把IT指令做为一个单独的bcond指令来处理,比如上图中的代码,转换成c语言后如下:
if (!r0) {
rt = 0x58586a70;
}
bcond指令处理
编译优化
生成def和use关系
cmp
cmp r0, r1
// def = APSR
// use = ln, lm
movw
MOVW R1, #0X7571
// def = r1
// use = empty
movt
MOVT R1, #0X791
// def = r1
// use = r1, 这个地方是把0x791移到r1的高位,所以它的use也包含r1
add
adds r0, 1
// def = apsr, r0; add的某些指令会设置apsr寄存器,所以它的def会有多个,这也导致了普通的活跃性分析的代码,要做修改才能适应反混淆
// use = empty,
blx addr
// def = r0, r1
// use = r0, r1, r2, r3
// 我在早期设计中使用了别名分析,因为无法准确的判断函数的传入参数,出席那了大量的错误,不得已每次进入函数都要def一遍所有的内存变量,生成了大量的垃圾指令,后来被我全部关闭了
prologue
epilogue
活跃性分析
int minst_blk_liveness_calc(struct minst_blk *blk)
{
struct minst_node *succ;
struct minst *minst;
struct bitset in = { 0 }, out = { 0 }, use = {0};
int i, changed = 1;
for (i = blk->allinst.len - 1; i >= 0; i--) {
minst = blk->allinst.ptab[i];
bitset_clear(&minst->in);
bitset_clear(&minst->out);
}
while (changed) {
changed = 0;
for (i = blk->allinst.len - 1; i >= 0; i--) {
minst = blk->allinst.ptab[i];
if (minst->flag.dead_code || (minst->cfg && minst->cfg->flag.dead_code)) continue;
bitset_clone(&in, &minst->in);
bitset_clone(&out, &minst->out);
bitset_clone(&use, &minst->use);
bitset_clone(&minst->in, bitset_or(&use, bitset_sub(&minst->out, &minst->def)));
for (succ = &minst->succs; succ; succ = succ->next) {
if (succ->minst)
bitset_or(&minst->out, &succ->minst->in);
}
if (!changed && (!bitset_is_equal(&in, &minst->in) || !bitset_is_equal(&out, &minst->out)))
changed = 1;
}
}
bitset_uninit(&in);
bitset_uninit(&out);
bitset_uninit(&use);
return 0;
}
死代码删除
1. mov r0, 0
2. mov r0, 1
到达定值分析
假如你也打算深入分析混淆壳,建议认真读下 @MCIC的17章,Dataflow Analysis。
常量传播
1: c <- 1
2: b <- c + 1
3: cmp c, b
假如某条指令被常量定值以后,后续所有使用过此条指令的代码都要根据自己的语义进行更新。
核心反混淆
1. 判断是否有混淆
1.1 找到其支配节点(Dominators)
while (1) { <-- 这个就是支配节点
if (a)
....
if (b)
....
}
2. 混淆的原理
简单的例子
printf("hello world!");
int a = 0;
while (1) {
if (a > -1) {
a = 1;
}
else if (a > 0) {
a = 2;
}
else if (a > 1) {
a = 3;
}
...
else if (a > 99) {
a = 101;
}
else if (a == 101) {
printf("hello world\n");
break;
}
}
复杂的例子
num = 17;
ret = isprime(num);
if (ret) {
printf("%d is prime", num);
}
else {
printf("%d not is prime", num);
}
num = 17;
ret = isprime(num);
a = 100;
while (1) {
if (a > 99) {
a = 101;
}
else if (a > 100) {
a = 102;
}
...
else if (a > 199) {
a = 201;
if (ret) {
a = 301;
}
}
else if (a == 201) {
a = 202;
}
else if (a == 203) {
printf("%d not is prime", num);
break;
}
else if (a > 300) {
a = 302;
}
else if (a > 301) {
printf("%d is prime", num);
break;
}
}
算法实现
步骤1 基本参数分析
代码:minst_dob_analyze
步骤2 混淆拓展
num = 17;
ret = isprime(num);
if (ret) {
printf("%s is prime\n", ret);
}
a = 100;
ret = isprime(num);
b = 200;
if (ret) b = 300;
while (1) {
if (a == 100) {
a = 101; // cfg1
}
else if (a == 101) {
a = b; // cfg2, 注意这里
}
else if (a == 200) {
// cfg3
}
else if (a == 300) {
// cfg4
}
}
else if (a == 101) {
a = b;
c = a;
a = 200;
if (a == c) {
continue;
}
else {
a = 300;
}
a == b;
if (a == 200) {
}
else if (a == 300) {
}
if (a == 100) {
}
和
a = 100;
步骤3 cfg节点分类
分类算法非常简单,用dfs即可:
1. 从头节点开始遍历,可达的所有节点都标注为 csm_in
2. 从混淆的支配节点开始遍历,可达的所有节点为csm
3. 出口节点标注为csm_out
4. 遍历csm_out节点的前驱节点,假如前驱节点的所有后继节点都为csm_out,则它也为csm_out
5. 重复4,一直到其不动点为止
步骤4 遍历常量定值点
步骤5 定值点跟踪
步骤6 编译优化
步骤7 到最小不动点结束
https://github.com/baikaishiuc/fastvm/wiki/sub_367c
遗留的问题
为什么不用ssa
为什么没生成新的so
能否使用别名分析
看雪ID:baikaishiu
https://bbs.pediy.com/user-630711.htm
*本文由看雪论坛 baikaishiu 原创。
推荐文章++++
好书推荐